﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Skugo.Shared;

namespace Skugo
{
    /// <summary>
    /// The plugin provides functionality for creating a remote instance,
    /// as well as sending, and receiving data to and from that instance
    /// </summary>
    public abstract class Plugin : IDisposable
    {
        /// <summary>
        /// Store the guid for this object.
        /// </summary>
        public readonly Int64 InstanceGuid;

        /// <summary>
        /// Store the remote type name.
        /// </summary>
        private String RemoteTypeName;

        /// <summary>
        /// Maintains the connection with the external plugin.
        /// </summary>
        protected readonly IWriter Writer;

        /// <summary>
        /// The plugin container contains the list of all plugins (including us).
        /// </summary>
        private PluginContainer Plugins;

        /// <summary>
        /// A guid counter that lets us assign a unique id to a new instance of a plugin.
        /// </summary>
        private static Int64 GuidCounter;

        public Plugin(String remoteTypeName, Client client)
        {
            // Assign the instance guid
            this.InstanceGuid = ++GuidCounter;

            // Store the remote type name
            this.RemoteTypeName = remoteTypeName;

            // Store the plugins
            this.Plugins = client.LocalPlugins;

            // Store the writer
            this.Writer = client.Connection;

            // Add itself to the list of plugins
            this.Plugins.AddPlugin(this);
        }

        public virtual void Dispose()
        {
            // Destroy the remote type
            this.DestroyRemote();

            // Remove itself from the plugins
            this.Plugins.RemovePlugin(this);
        }

        /// <summary>
        /// Send data to our external counterpart(s).
        /// </summary>
        /// <param name="data">The data that we want to send.</param>
        protected void Send(Byte[] data)
        {
            // Send the data over the link
            this.Send(data, PacketType.Message);
        }

        private void Send(Byte[] data, PacketType type)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Get the length of the data
            Int32 length = 0;
            if (data != null)
            {
                length = data.Length;
            }

            // Write out the packet data
            outgoing.Write((UInt32)length);
            outgoing.Write((Byte)type);
            outgoing.Write((Int64)this.InstanceGuid);

            // Write the provided data
            if (data != null)
            {
                outgoing.Write(data);
            }

            // Send the packet out
            this.Writer.Write(stream.ToArray());

            // Finish off the stream
            outgoing.Close();
        }

        public BinaryWriter BeginCreateRemotePlugin()
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write the remote plugin name that we want to create (reflection name)
            outgoing.WriteAsciiString(this.RemoteTypeName);

            // Return the stream so that the user can add data to it
            return outgoing;
        }

        public void EndCreateRemotePlugin(BinaryWriter outgoing)
        {
            // Get the memory stream out of the writer
            var stream = (MemoryStream)outgoing.BaseStream;

            // Send the data that will create the remote plugin
            this.Send(stream.ToArray(), PacketType.Create);
        }

        /// <summary>
        /// Create the remote plugin.
        /// </summary>
        public void CreateRemotePlugin()
        {
            var outgoing = this.BeginCreateRemotePlugin();
            this.EndCreateRemotePlugin(outgoing);
        }

        /// <summary>
        /// Destroy the remote plugin (helper).
        /// </summary>
        private void DestroyRemote()
        {
            // Write out the destroy message
            this.Send(null, PacketType.Destroy);
        }

        /// <summary>
        /// Internally called when packet data is received from our external counterpart(s).
        /// </summary>
        /// <param name="data">The packet data.</param>
        internal void Receive(Byte[] data, PacketType type)
        {
            // If the packet type is just a message, then it is intended for the user
            if (type == PacketType.Message)
            {
                this.OnReceive(data);
            }
            else if (type == PacketType.DragDrop)
            {
                // Create a binary reader to parse the data
                var incoming = data.ToBinaryReader();

                // Figure out who the data came from
                var sourceGuid = incoming.ReadInt64();

                // Grab the source plugin from the container by guid
                var sourcePlugin = this.Plugins.GetPlugin(sourceGuid);

                // Read the size of the drag drop data
                var dragDropDataSize = incoming.ReadInt32();

                // Read the rest of the data into a single byte buffer
                // This is the drag drop data that was serialized by the remote source plugin
                var dragDropRawData = incoming.ReadBytes(dragDropDataSize);

                // Create the object that represents the remote data
                var dragDropObjectData = sourcePlugin.DeserializeDragDropData(dragDropRawData);

                // Make sure we were able to get the data from the source
                if (dragDropObjectData != null)
                {
                    // Finally, read the rest of the data as context data from the drop plugin
                    var contextData = incoming.ReadBytes(data.Length - (Int32)incoming.BaseStream.Position);

                    // If anyone is listening to the event...
                    this.OnDrop(contextData, sourcePlugin, dragDropObjectData);
                }
            }
            else
            {
                // Otherwise, we had no idea what it was
                throw new Exception("Unknown packet type encountered");
            }
        }

        /// <summary>
        /// Called when packet data is received from our external counterpart(s).
        /// </summary>
        /// <param name="data">The packet data.</param>
        protected abstract void OnReceive(Byte[] data);

        /// <summary>
        /// Called when an item gets dragged and dropped onto our external counterpart.
        /// </summary>
        /// <param name="data">Any data we sent over with the event.</param>
        /// <param name="source">The source plugin where the drag originated from.</param>
        /// <param name="sourceData">The contextual data for the source plugin (what was being dragged).</param>
        protected virtual void OnDrop(Byte[] data, Plugin source, Object sourceData)
        {
            // The default behavior is to do nothing, we'll let people handle it on their own
        }

        /// <summary>
        /// A user implemented function that deserializes the drag
        /// drop data that they sent from their remote plugin.
        /// </summary>
        /// <param name="data">The data to be deserialized.</param>
        /// <returns>
        /// The object which the data represents, or null if
        /// for any reasonthe drag drop event should not be completed.
        /// </returns>
        protected virtual Object DeserializeDragDropData(Byte[] data)
        {
            throw new Exception("The user did not implement the 'DeserializeDragDropData' function, even though they sent drag drop data");
        }
    }

    /// <summary>
    /// A plugin container contains a list of plugins.
    /// </summary>
    public class PluginContainer
    {
        /// <summary>
        /// Store a map of guid to all the plugins that were registered.
        /// </summary>
        private Dictionary<Int64, Plugin> Plugins = new Dictionary<Int64, Plugin>();

        /// <summary>
        /// Add a plugin to the container.
        /// </summary>
        /// <param name="plugin">The plugin to be added.</param>
        public void AddPlugin(Plugin plugin)
        {
            this.Plugins.Add(plugin.InstanceGuid, plugin);
        }

        /// <summary>
        /// Remove a plugin to from container.
        /// </summary>
        /// <param name="plugin">The plugin to be removed.</param>
        public void RemovePlugin(Plugin plugin)
        {
            this.Plugins.Remove(plugin.InstanceGuid);
        }

        /// <summary>
        /// Get a plugin by guid.
        /// </summary>
        /// <param name="pluginGuid">The guid associated with the plugin.</param>
        /// <returns>Returns the plugin associated with the given guid, or null if it was not found.</returns>
        public Plugin GetPlugin(Int64 pluginGuid)
        {
            return this.Plugins.FindValue(pluginGuid);
        }

        /// <summary>
        /// Distroys all associated plugins.
        /// </summary>
        public void DestroyAllPlugins()
        {
            // Create a list of plugins, so we can iterate and remove as we go
            var plugins = new List<Plugin>(this.Plugins.Values);

            // Loop through all plugins and dispose each one (which removes it)
            foreach (var plugin in plugins)
            {
                // This will actually remove the plugin from the map
                plugin.Dispose();
            }
        }
    };
}
